<?php
// +----------------------------------------------------------------------
// | INPHP
// | Copyright (c) https://inphp.cc All rights reserved.
// | 该文件源码由INPHP官方提供，使用协议以INPHP官方公告为准。
// +----------------------------------------------------------------------
// | 支付处理
// +----------------------------------------------------------------------
namespace app\finance\onlinePay\wechat;

use app\finance\model\CashierModel;
use app\finance\onlinePay\IPay;
use Inphp\Core\Object\Message;
use Inphp\Core\Services\Http\Response;
use Inphp\Core\Util\Log;
use WeChatPay\Crypto\AesGcm;
use WeChatPay\Crypto\Rsa;
use WeChatPay\Formatter;

class pay implements IPay
{
    /**
     * 创建付款订单
     * @param array $arg
     * @return Message
     */
    public function prepay(array $arg = []): Message
    {
        // TODO: Implement prepay() method.
        //支付金额
        $amount = $arg["amount"] ?? 0;
        $amount = is_numeric($amount) && $amount > 0 ? $amount : 0;
        if ($amount <= 0) {
            return httpMessage("支付金额必须大于0");
        }
        //收银单ID
        $cashierId = $arg["cashierId"] ?? 0;
        if (empty($cashierId)) {
            return httpMessage("缺少收银单号");
        }
        //支付参数
        $params = $arg["params"] ?? [];
        if (empty($params)) {
            //return httpMessage("缺少支付参数");
        }
        //
        $clientName = $params["clientName"] ?? "JSAPI";
        $clientName = strtoupper($clientName);
        $clientName = $clientName === "MP" || $clientName === "OA" || $clientName === "MINI_PROGRAM" ? "JSAPI" : $clientName;
        $clientName = in_array($clientName, ["H5", "APP", "JSAPI"]) ? $clientName : "JSAPI";
        //JSAPI 需要OPENID
        if ($clientName === "JSAPI") {
            //需要OPEN ID
            $openId = $params["openId"] ?? null;
            if (empty($openId)) {
                return httpMessage(1, "缺少OPENID");
            }
        }
        //可能是客户端自定义APP ID
        $appId = $params["appId"] ?? null;
        //取配置
        $payments = CashierModel::onlinePayments();
        $config = $payments["wechat"] ?? null;
        if (empty($config)) {
            return httpMessage("缺少配置参数");
        }
        if (!$config["enable"]) {
            return httpMessage("该支付方式暂未开放");
        }
        $appList = $config["appList"] ?? [];
        if (empty($appList)) {
            return httpMessage("没有可用的支付应用");
        }
        $app = !empty($appId) ? ($appList[$appId] ?? reset($appList)) : reset($appList);
        $app = is_array($app) ? $app : [];
        if (empty($app)) {
            return httpMessage("未配置的支付应用");
        }
        $appId = $app["appId"] ?? $appId;
        if (empty($appId)) {
            return httpMessage("缺少支付应用ID");
        }
        //域名地址
        $host = getHost();
        //异步通知地址
        $notifyUrl = $host."/finance/pay/notify/wechat_{$appId}";
        //$notifyUrl = $app["notifyUrl"] ?? $notifyUrl;
        //同步地址
        $redirectUrl = $host."/finance/pay/callback/wechat_{$appId}";
        //$redirectUrl = $app["redirectUrl"] ?? $redirectUrl;
        //订单号前缀
        $outTradeNoPrefix = $app["outTradeNoPrefix"] ?? "";
        //生成单号
        $outTradeNo = $outTradeNoPrefix.$cashierId;
        //构造数据
        try {
            //公钥
            $publicKeyInstance = \WeChatPay\Crypto\Rsa::from($app["cert"], \WeChatPay\Crypto\Rsa::KEY_TYPE_PUBLIC);
            //证书私钥
            $privateKeyInstance = \WeChatPay\Crypto\Rsa::from($app["key"], \WeChatPay\Crypto\Rsa::KEY_TYPE_PRIVATE);
            //取得证书序列号
            $platformCertSN = \WeChatPay\Util\PemUtil::parseCertificateSerialNo($app["cert"]);
        } catch (\Exception $exception) {
            return httpMessage($exception->getMessage());
        }
        //构造APIv3客户端
        $instance = \WeChatPay\Builder::factory([
            "mchid"     => (string) $app["mchId"],
            "serial"    => (string) $app["certSN"],
            "privateKey"=> $privateKeyInstance,
            "certs"     => [
                $platformCertSN => $publicKeyInstance
            ]
        ]);
        if (!$instance) {
            return httpMessage("未能构建v3支付客户对象");
        }
        $chain = match ($clientName) {
            "APP"       => "v3/pay/transactions/app",
            //"JSAPI"     => "v3/pay/transactions/jsapi",
            "H5"        => "v3/pay/transactions/h5",
            default     => "v3/pay/transactions/jsapi"
        };
        $description = $arg["subject"] ?? "";
        $description .= $arg["info"] ?? "";
        //
        $body = [
            "appid"     => (string) $app["appId"],
            "mchid"     => (string) floor($app["mchId"]),
            "description" => !empty($description) ? $description : "默认付款订单",
            "out_trade_no"=> (string) $outTradeNo,
            "notify_url"=> $notifyUrl,
            "amount"    => [
                "total"     => floor($amount * 100),
                "currency"  => "CNY"
            ],
            //附加信息，原样返回
            "attach"    => ""
        ];
        if ($clientName === "JSAPI") {
            $body += [
                "payer"     => [
                    "openid"    => $openId
                ]
            ];
        }
        //
        try{
            $response = $instance->chain($chain)
                ->post(["json" => $body]);
            if ($response->getStatusCode() === 200) {
                $content = $response->getBody()->getContents();
                $json = !empty($content) ? (@json_decode($content, true) ?? null) : null;
                if (is_array($json) && isset($json["prepay_id"])) {
                    if ($clientName === "H5") {
                        return httpMessage([
                            "h5_url"    => $json["h5_url"]
                        ]);
                    } else {
                        //正确进行相应的签名
                        $params = [
                            "appId"     => (string) $appId,
                            "timeStamp" => (string) time(),
                            "nonceStr"  => Formatter::nonce(),
                            "package"   => "prepay_id={$json["prepay_id"]}"
                        ];
                        $params += [
                            "paySign"   => Rsa::sign(Formatter::joinedByLineFeed(...array_values($params)), $privateKeyInstance),
                            "signType"  => "RSA"
                        ];
                        return httpMessage([
                            "params"    => $params,
                            "data"      => $params
                        ]);
                    }
                }
                return httpMessage("未取得预支付交易会话标识".(isset($json["code"]) ? ("，错误码：{$json["code"]}/{$json["message"]}") : ""));
            }
            return httpMessage("调用微信支付接口失败，未能正确返回数据，HTTP状态码：{$response->getStatusCode()}");
        } catch (\Exception $exception) {
            //错误记录到文件
            $date = date("Ymd");
            Log::writeToEnd(RUNTIME."/logs/finance/wechat/pay/{$date}.txt", $exception);
            return httpMessage(1, $exception->getMessage());
        }
    }

    /**
     * 同步回调
     * @param string|null $appId
     * @return mixed
     */
    public function callback(?string $appId = null): mixed
    {
        // TODO: Implement callback() method.
        return \redirect("/");
    }

    /**
     * 异步通知
     */
    public function notify(?string $appId = null)
    {
        // TODO: Implement notify() method.
        $client = getClient();
        $this->saveLog(json_encode(["headers" => $client->header, "post" => $client->post, "get" => $client->get, "raw" => $client->rawData], 256));
        //从头部获取参数
        $inWechatPaySignature = HEADERS("Wechatpay-Signature");
        $inWechatPayTimestamp = HEADERS("Wechatpay-Timestamp");
        $inWechatPayTimestamp = is_numeric($inWechatPayTimestamp) ? $inWechatPayTimestamp : 0;
        $inWechatPayNonce = HEADERS("Wechatpay-Nonce");
        $inWechatPaySerial = HEADERS("Wechatpay-Serial");
        $inWechatPayRequestId = HEADERS("Request-ID");
        //JSON已自动转化为POST参数
        $inBody = $client->rawData;
        //取配置
        $payments = CashierModel::onlinePayments();
        $config = $payments["wechat"] ?? null;
        if (empty($config)) {
            $this->saveLog("缺少配置参数");
            getHttpResponse()->error(404, json_encode(["code" => "FAIL", "message" => "缺少配置参数"], 256), Response::CONTENT_TYPE_APPLICATION_JSON);
            return;
        }
        $appList = $config["appList"] ?? [];
        if (empty($appList)) {
            $this->saveLog("没有可用的支付应用");
            getHttpResponse()->error(404, json_encode(["code" => "FAIL", "message" => "没有可用的支付应用"], 256), Response::CONTENT_TYPE_APPLICATION_JSON);
            return;
        }
        $app = !empty($appId) ? ($appList[$appId] ?? reset($appList)) : reset($appList);
        $app = is_array($app) ? $app : [];
        if (empty($app)) {
            $this->saveLog("未配置的支付应用");
            getHttpResponse()->error(404, json_encode(["code" => "FAIL", "message" => "未配置的支付应用"], 256), Response::CONTENT_TYPE_APPLICATION_JSON);
            return;
        }
        $appId = $app["appId"] ?? $appId;
        if (empty($appId)) {
            $this->saveLog("缺少支付应用ID");
            getHttpResponse()->error(404, json_encode(["code" => "FAIL", "message" => "缺少支付应用ID"], 256), Response::CONTENT_TYPE_APPLICATION_JSON);
            return;
        }
        //开发模式
        $dev = false;
        //
        $publicKeyInstance = Rsa::from($app["cert"], Rsa::KEY_TYPE_PUBLIC);
        // 检查通知时间偏移量，允许5分钟之内的偏移
        $timeOffsetStatus = abs(time() - (int)$inWechatPayTimestamp);
        if ($timeOffsetStatus >= 300) {
            $this->saveLog("时间偏移过大");
            getHttpResponse()->error(404, json_encode(["code" => "FAIL", "message" => "时间偏移过大"], 256), Response::CONTENT_TYPE_APPLICATION_JSON);
            return;
        }
        $verifiedStatus = Rsa::verify(
            // 构造验签名串
            Formatter::joinedByLineFeed($inWechatPayTimestamp, $inWechatPayNonce, $inBody),
            $inWechatPaySignature,
            $publicKeyInstance
        );
        if (!$verifiedStatus) {
            $this->saveLog("构造验签名单失败");
            getHttpResponse()->error(404, json_encode(["code" => "FAIL", "message" => "构造验签名单失败"], 256), Response::CONTENT_TYPE_APPLICATION_JSON);
            return;
        }
        // 转换通知的JSON文本消息为PHP Array数组
        $inBodyArray = (array) json_decode($inBody, true);
        // 使用PHP7的数据解构语法，从Array中解构并赋值变量
        ['resource' => [
            'ciphertext'      => $ciphertext,
            'nonce'           => $nonce,
            'associated_data' => $aad
        ]] = $inBodyArray;
        // 加密文本消息解密
        $inBodyResource = AesGcm::decrypt($ciphertext, $app["apiKey"], $nonce, $aad);
        // 把解密后的文本转换为PHP Array数组
        $inBodyResourceArray = (array)json_decode($inBodyResource, true);
        // print_r($inBodyResourceArray);// 打印解密后的结果
        $this->saveLog(json_encode($inBodyResourceArray, 256));
        //
        //关键数据需要记录的：trade_type(付款方式/交易类型：JSAP、APP...)
        //trade_state 交易状态
        if ($inBodyResourceArray["trade_state"] == "SUCCESS") {
            $cashierId = $inBodyResourceArray["out_trade_no"];
            //订单号前缀
            $outTradeNoPrefix = $app["outTradeNoPrefix"] ?? "";
            //如果携带有前缀，需要去除前缀再使用
            if (!empty($outTradeNoPrefix) && stripos($cashierId, $outTradeNoPrefix) === 0) {
                $cashierId = substr($cashierId, strlen($outTradeNoPrefix));
            }
            $payedAmount = $inBodyResourceArray["amount"]["total"];
            $payerId = $inBodyResourceArray["payer"]["openid"];
            $msg = CashierModel::payed($cashierId, bcdiv($payedAmount, 100, 2), $inBodyResourceArray["transaction_id"], $payerId, "wechat", $appId, $inBodyResource, $dev);
            if ($msg->error !== 0) {
                $this->saveLog(json_encode(["cashierId" => $cashierId, "message" => "未能处理付款通知"]));
                getHttpResponse()->error(404, json_encode(["code" => "FAIL", "message" => "未能处理付款通知"], 256), Response::CONTENT_TYPE_APPLICATION_JSON);
            }
        }
        getHttpResponse()->withJson(["code" => "SUCCESS", "message" => "数据已接收"])->end();
    }
    private function saveLog(string $text) {
        $date = date("Ymd");
        Log::writeToEnd(RUNTIME."/logs/finance/wechat/pay/{$date}.txt", date("Y/m/d H:i:s")."\r\n".$text);
    }
}